using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Framework.Core.CommandLine
{
    public class CommandLineParser : ICommandLineParser
    {
        private static readonly Regex CommandLineRegex = new Regex(@"/(?<Command>[^:/]*):*(?(?=[""\{])[""\{]{1}(?<Value>[^""]*)[""\}]{1}\s*|(?<Value>[^:/]*)\s*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

        public T Parse<T>(string[] args)
        {
            if (args == null)
            {
                throw new ArgumentNullException("args");
            }

            var commandLineArgs = Activator.CreateInstance<T>();

            var properties = commandLineArgs.GetType().GetProperties();

            foreach (var p in properties)
            {
                var c = p.GetCustomAttributes(typeof(CommandLineArgsAttribute), false).Cast<CommandLineArgsAttribute>().
                        FirstOrDefault();

                if (c != null)
                {
                    SetPropertyValue(commandLineArgs, p, c, args, GetArgNames(c, p));
                }
            }

            return commandLineArgs;
        }

        private static string GetArgName(CommandLineArgsAttribute c, PropertyInfo p)
        {
            var result = c.Name;

            if (string.IsNullOrWhiteSpace(result))
            {
                result = c.ShortName;
            }

            if (string.IsNullOrWhiteSpace(result))
            {
                result = p.Name;
            }

            return result;
        }

        private static string[] GetArgNames(CommandLineArgsAttribute c, PropertyInfo p)
        {
            var result = new List<string>();

            if (!string.IsNullOrWhiteSpace(c.Name))
            {
                result.Add(c.Name);
            }

            if (!string.IsNullOrWhiteSpace(c.ShortName))
            {
                result.Add(c.ShortName);
            }

            if (!string.IsNullOrWhiteSpace(p.Name))
            {
                result.Add(p.Name);
            }

            return result.ToArray();
        }

        private static object Parse(string stringValue, Type t)
        {
            var type = (Nullable.GetUnderlyingType(t)) ?? t;

            var trimmedString = stringValue.SafeTrim();

            if (type == typeof(bool))
            {
                if (string.IsNullOrWhiteSpace(trimmedString))
                {
                    return true;
                }

                return (string.Equals("true", trimmedString, StringComparison.InvariantCultureIgnoreCase));
            }

            if (type == typeof(string))
            {
                return string.Join(" ", trimmedString.Split(','));
            }

            if (type.IsEnum)
            {
                return Enum.Parse(type, trimmedString);
            }

            var method = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null,
                                        new[] { typeof(string) }, null);

            return method.Invoke(null, new object[] { trimmedString });
        }

        private static void SetPropertyValue<T>(T commandLineArgs, PropertyInfo propertyInfo,
                                          CommandLineArgsAttribute commandLineArgsAttribute, IEnumerable<string> args,
                                          string[] patterns)
        {
            EnsureArgumentRequirements(commandLineArgsAttribute, propertyInfo);
            var value = GetValue(args, patterns, propertyInfo.PropertyType, commandLineArgsAttribute);
            propertyInfo.SetValue(commandLineArgs, value, null);
            EnsureValue(value, commandLineArgsAttribute, propertyInfo);
        }

        private static object GetValue(IEnumerable<string> args, string[] patterns, Type t, CommandLineArgsAttribute commandLineArgsAttribute)
        {
            foreach (var arg in args)
            {
                var match = CommandLineRegex.Match(arg);

                while (match.Success)
                {
                    if (patterns.Any(p => AreEquals(match, p)))
                    {
                        return Parse(match.Groups["Value"].Value.SafeTrim(), t);
                    }
                    match = match.NextMatch();
                }
            }

            if (t == typeof(bool))
            {
                return false;
            }

            return commandLineArgsAttribute.DefaultValue;
        }

        private static bool AreEquals(Match match, string pattern)
        {
            return string.Equals(match.Groups["Command"].Value.SafeTrim(), pattern, StringComparison.InvariantCultureIgnoreCase);
        }

        private static bool HasArgumentRequirements(CommandLineArgsAttribute c, ArgumentRequirements a)
        {
            return (c.ArgumentRequirements & a) != ArgumentRequirements.None;
        }

        private static void EnsureArgumentRequirements(CommandLineArgsAttribute c, PropertyInfo p)
        {
            if (c.ArgumentRequirements == ArgumentRequirements.None)
            {
                c.ArgumentRequirements = ArgumentRequirements.Required;
            }

            if (HasArgumentRequirements(c, ArgumentRequirements.Required) &&
                HasArgumentRequirements(c, ArgumentRequirements.Optional))
            {
                throw new ArgumentException(string.Format("arg {0} can't be Optional and Required", GetArgName(c, p)));
            }
            if (!HasArgumentRequirements(c, ArgumentRequirements.Optional) &&
                (!string.IsNullOrWhiteSpace(c.DefaultValue.SafeToString())))
            {
                throw new ArgumentException(string.Format("arg {0} : DefaultValue forbidden when arg is not Optional",
                                                          GetArgName(c, p)));
            }
        }

        private static void EnsureValue(object value, CommandLineArgsAttribute c, PropertyInfo p)
        {
            if (HasArgumentRequirements(c, ArgumentRequirements.Required) && value == null)
            {
                throw new ArgumentException(string.Format("Arg {0} value is Required", GetArgName(c, p)));
            }
        }
    }
}